//
//  NSObject+HCTB.m
//  HCToolBox
//
//  Created by Hotacool on 2019/3/19.
//

#import "NSObject+HCTB.h"
#import <objc/runtime.h>
#import "HCWeakObject.h"

static char hctb_associatedObjectKey;
@implementation NSObject (HCTB)

+ (BOOL)swizzleMethodsFrom:(Class)fromClass selector:(SEL)originSelector to:(Class)toClass newSelector:(SEL)newSelector {
    if (!fromClass || !toClass) {
        return NO;
    }
    
    Method oriMethod = class_getInstanceMethod(fromClass, originSelector);
    Method newMethod = class_getInstanceMethod(toClass, newSelector);
    if (!newMethod) {
        return NO;
    }
    
    BOOL isAddedMethod = class_addMethod(fromClass, originSelector, method_getImplementation(newMethod), method_getTypeEncoding(newMethod));
    if (isAddedMethod) {
        // 如果 class_addMethod 成功了，说明之前 fromClass 里并不存在 originSelector，所以要用一个空的方法代替它，以避免 class_replaceMethod 后，后续 toClass 的这个方法被调用时可能会 crash
        IMP oriMethodIMP = method_getImplementation(oriMethod) ?: imp_implementationWithBlock(^(id selfObject) {});
        const char *oriMethodTypeEncoding = method_getTypeEncoding(oriMethod) ?: "v@:";
        class_replaceMethod(toClass, newSelector, oriMethodIMP, oriMethodTypeEncoding);
    } else {
        method_exchangeImplementations(oriMethod, newMethod);
    }
    return YES;
}

+ (HCPropertyAttributes *)getPropertyAttributes:(char *)propertyName withCLass:(Class)cls {
    if (strlen(propertyName) == 0) {
        return NULL;
    }
    objc_property_t property = class_getProperty(cls, propertyName);
    const char * const attrString = property_getAttributes(property);
        if (!attrString) {
            fprintf(stderr, "ERROR: Could not get attribute string from property %s\n", property_getName(property));
            return NULL;
        }

        if (attrString[0] != 'T') {
            fprintf(stderr, "ERROR: Expected attribute string \"%s\" for property %s to start with 'T'\n", attrString, property_getName(property));
            return NULL;
        }

        const char *typeString = attrString + 1;
        const char *next = NSGetSizeAndAlignment(typeString, NULL, NULL);
        if (!next) {
            fprintf(stderr, "ERROR: Could not read past type in attribute string \"%s\" for property %s\n", attrString, property_getName(property));
            return NULL;
        }

        size_t typeLength = next - typeString;
        if (!typeLength) {
            fprintf(stderr, "ERROR: Invalid type in attribute string \"%s\" for property %s\n", attrString, property_getName(property));
            return NULL;
        }

        // allocate enough space for the structure and the type string (plus a NUL)
        HCPropertyAttributes *attributes = calloc(1, sizeof(HCPropertyAttributes) + typeLength + 1);
        if (!attributes) {
            fprintf(stderr, "ERROR: Could not allocate ext_propertyAttributes structure for attribute string \"%s\" for property %s\n", attrString, property_getName(property));
            return NULL;
        }
        
        // copy the type string. seems something wrong!
        char *_type = strncpy(attributes->type, typeString, typeLength);
        attributes->type[typeLength] = '\0';

        // if this is an object type, and immediately followed by a quoted string...
        if (typeString[0] == *(@encode(id)) && typeString[1] == '"') {
            // we should be able to extract a class name
            const char *className = typeString + 2;
            next = strchr(className, '"');

            if (!next) {
                fprintf(stderr, "ERROR: Could not read class name in attribute string \"%s\" for property %s\n", attrString, property_getName(property));
                goto errorOut;
            }

            if (className != next) {
                size_t classNameLength = next - className;
                char trimmedName[classNameLength + 1];

                strncpy(trimmedName, className, classNameLength);
                trimmedName[classNameLength] = '\0';

                // attempt to look up the class in the runtime
                attributes->objectClass = objc_getClass(trimmedName);
            }
        } else {
            // 非对象类型，获取对应值类型的encode type
            attributes->typeEncode = _type;
        }

        if (*next != '\0') {
            // skip past any junk before the first flag
            next = strchr(next, ',');
        }

        while (next && *next == ',') {
            char flag = next[1];
            next += 2;

            switch (flag) {
            case '\0':
                break;

            case 'R':
                attributes->readonly = YES;
                break;

            case 'C':
                attributes->memoryManagementPolicy = HCMemoryPolicyCopy;
                break;

            case '&':
                attributes->memoryManagementPolicy = HCMemoryPolicyRetain;
                break;

            case 'N':
                attributes->nonatomic = YES;
                break;

            case 'G':
            case 'S':
                {
                    const char *nextFlag = strchr(next, ',');
                    SEL name = NULL;

                    if (!nextFlag) {
                        // assume that the rest of the string is the selector
                        const char *selectorString = next;
                        next = "";

                        name = sel_registerName(selectorString);
                    } else {
                        size_t selectorLength = nextFlag - next;
                        if (!selectorLength) {
                            fprintf(stderr, "ERROR: Found zero length selector name in attribute string \"%s\" for property %s\n", attrString, property_getName(property));
                            goto errorOut;
                        }

                        char selectorString[selectorLength + 1];

                        strncpy(selectorString, next, selectorLength);
                        selectorString[selectorLength] = '\0';

                        name = sel_registerName(selectorString);
                        next = nextFlag;
                    }

                    if (flag == 'G')
                        attributes->getter = name;
                    else
                        attributes->setter = name;
                }

                break;

            case 'D':
                attributes->dynamic = YES;
                attributes->ivar = NULL;
                break;

            case 'V':
                // assume that the rest of the string (if present) is the ivar name
                if (*next == '\0') {
                    // if there's nothing there, let's assume this is dynamic
                    attributes->ivar = NULL;
                } else {
                    attributes->ivar = next;
                    next = "";
                }

                break;

            case 'W':
                attributes->weak = YES;
                break;

            case 'P':
                attributes->canBeCollected = YES;
                break;

            case 't':
                fprintf(stderr, "ERROR: Old-style type encoding is unsupported in attribute string \"%s\" for property %s\n", attrString, property_getName(property));

                // skip over this type encoding
                while (*next != ',' && *next != '\0')
                    ++next;

                break;

            default:
                fprintf(stderr, "ERROR: Unrecognized attribute string flag '%c' in attribute string \"%s\" for property %s\n", flag, attrString, property_getName(property));
            }
        }

        if (next && *next != '\0') {
            fprintf(stderr, "Warning: Unparsed data \"%s\" in attribute string \"%s\" for property %s\n", next, attrString, property_getName(property));
        }

        if (!attributes->getter) {
            // use the property name as the getter by default
            attributes->getter = sel_registerName(property_getName(property));
        }

        if (!attributes->setter) {
            const char *propertyName = property_getName(property);
            size_t propertyNameLength = strlen(propertyName);

            // we want to transform the name to setProperty: style
            size_t setterLength = propertyNameLength + 4;

            char setterName[setterLength + 1];
            strncpy(setterName, "set", 3);
            strncpy(setterName + 3, propertyName, propertyNameLength);

            // capitalize property name for the setter
            setterName[3] = (char)toupper(setterName[3]);

            setterName[setterLength - 1] = ':';
            setterName[setterLength] = '\0';

            attributes->setter = sel_registerName(setterName);
        }

        return attributes;

    errorOut:
        free(attributes);
        return NULL;
}

+ (BOOL)addDynamicProperty:(char *_Nullable)propertyName toClass:(Class)cls {
    BOOL result = false;
    objc_property_t property = class_getProperty(cls, propertyName);
    if (property) {
        HCPropertyAttributes *attributes = [self getPropertyAttributes:propertyName withCLass:cls];
        objc_AssociationPolicy policy = OBJC_ASSOCIATION_ASSIGN;
        switch (attributes->memoryManagementPolicy) {
            case HCMemoryPolicyRetain:
                policy = attributes->nonatomic ? OBJC_ASSOCIATION_RETAIN_NONATOMIC : OBJC_ASSOCIATION_RETAIN;
                break;
            case HCMemoryPolicyCopy:
                policy = attributes->nonatomic ? OBJC_ASSOCIATION_COPY_NONATOMIC : OBJC_ASSOCIATION_COPY;
                break;
            case HCMemoryPolicyAssign:
                break;
            default: {
                NSLog(@"Unrecognized property memory management policy %i", (int)attributes->memoryManagementPolicy);
                return result;
            }
        }
        
        id setblock;
        id getBlock;
        NSString *key = [NSString stringWithFormat:@"%p_%s", cls, propertyName];
        if (attributes->objectClass) {
            setblock = ^(id self,id value) {
                objc_setAssociatedObject(cls, (__bridge void *)key, value, OBJC_ASSOCIATION_COPY_NONATOMIC);
            };
            getBlock = ^id(id self){
                return objc_getAssociatedObject(cls, (__bridge void*)key);
            };
        } else {
            size_t length = strlen(attributes->typeEncode);
            char *type = malloc(length * sizeof(char));// copy char*
            strncpy(type, attributes->typeEncode, length);
            if (!type) {
                return result;
            }
            #define blockWithCaseType(C_TYPE)                               \
                if (strcmp(type, @encode(C_TYPE)) == 0) {                   \
                    setblock = ^(id self,C_TYPE var){                       \
                    NSValue *value = [NSValue value:&var withObjCType:type];\
                    objc_setAssociatedObject(cls, (__bridge void *)key, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);  \
                };                                                          \
                getBlock = ^C_TYPE (id self){                               \
                    NSValue *value = objc_getAssociatedObject(cls, (__bridge void*)key);   \
                    C_TYPE var;                                             \
                    [value getValue:&var];                                  \
                    return var;                                             \
                };                                                          \
            }
                blockWithCaseType(char);
                blockWithCaseType(unsigned char);
                blockWithCaseType(short);
                blockWithCaseType(int);
                blockWithCaseType(unsigned int);
                blockWithCaseType(long);
                blockWithCaseType(unsigned long);
                blockWithCaseType(long long);
                blockWithCaseType(float);
                blockWithCaseType(double);
                blockWithCaseType(BOOL);

                blockWithCaseType(CGPoint);
                blockWithCaseType(CGSize);
                blockWithCaseType(CGVector);
                blockWithCaseType(CGRect);
                blockWithCaseType(NSRange);
                blockWithCaseType(CFRange);
                blockWithCaseType(CGAffineTransform);
                blockWithCaseType(CATransform3D);
                blockWithCaseType(UIOffset);
                blockWithCaseType(UIEdgeInsets);
            #undef blockWithCaseType
        }
        IMP imp = imp_implementationWithBlock(setblock);
        if (!class_addMethod([cls class], attributes->setter, imp, "v@:@")) {
            return result;
        }
        IMP getImp = imp_implementationWithBlock(getBlock);
        if (!class_addMethod([cls class], attributes->getter, getImp, "@@:")) {
            return result;
        }
        free(attributes);
        result = YES;
    } else {
        NSLog(@"*** Could not copy property attributes for %@.%s", cls, propertyName);
    }
    return result;
}
#pragma mark - object bind
- (NSMutableDictionary*)hctb_associatedObjectsDictionary {
    NSMutableDictionary *associatedObjectsDictionary = objc_getAssociatedObject(self, &hctb_associatedObjectKey);
    if (!associatedObjectsDictionary) {
        associatedObjectsDictionary = [NSMutableDictionary dictionary];
        objc_setAssociatedObject(self, &hctb_associatedObjectKey, associatedObjectsDictionary, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return associatedObjectsDictionary;
}

- (void)hctb_setObject:(id)object forKey:(NSString*)key {
    if (hcObjectIsEmpty(key)) {
        return;
    }
    if (object) {
        [[self hctb_associatedObjectsDictionary] setObject:object forKey:key];
    } else {
        [[self hctb_associatedObjectsDictionary] removeObjectForKey:key];
    }
}

- (void)hctb_setObjectWeakly:(id)object forKey:(NSString*)key {
    HCWeakObject *weakObject = [HCWeakObject weakObjectWithTarget:object];
    [self hctb_setObject:weakObject forKey:key];
}

- (id)hctb_objectForKey:(NSString *)key {
    if (hcObjectIsEmpty(key)) {
        return nil;
    }
    id object = [[self hctb_associatedObjectsDictionary] objectForKey:key];
    if ([object isKindOfClass:[HCWeakObject class]]) {
        object = ((HCWeakObject*)object).target;
    }
    return object;
}

- (void)hctb_removeAllObjects {
    [[self hctb_associatedObjectsDictionary] removeAllObjects];
}

- (NSArray *)hctb_allKeys {
    return [[self hctb_associatedObjectsDictionary] allKeys];
}
@end
